feat: server-to-server Outlook/Exchange calendar sync#41326
feat: server-to-server Outlook/Exchange calendar sync#41326milton-rucks wants to merge 18 commits into
Conversation
…ndar_sync_state model Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…port Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Client-credentials token manager (in-memory, expiry-aware, single-flight), calendarView delta queries with 410 restart, Retry-After throttling backoff, getSchedule free/busy, and log sanitization helpers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…and scheduler wiring Batched per-user sync with delta-token lifecycle, full-snapshot deletion diffing, cancellation handling, per-user sync state, and cron job that re-deploys on settings changes (with state reset on semantic changes). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ated startup wiring Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…gine, mailbox resolver and sanitizer Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…xed Graph hosts, lint fixes and changeset Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…mpersonation Hand-rolled SOAP (FindItem/GetItem/GetUserAvailability/GetFolder) parsed with the repo's existing @xmldom/xmldom; vendored NTLMv2 (incl. pure-TS MD4, absent from OpenSSL 3) over a pinned keep-alive socket via node:https — zero new npm dependencies. TLS override is a scoped, explicit admin opt-in. The provider holds no Microsoft-cloud hostname (air-gap requirement). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ir-gap verification Air-gap proven three ways: static source scan for Microsoft-cloud hostnames, factory-level check that Graph is never constructed when provider=exchange-ews, and a runtime full-sync run asserting the only contacted host is the configured EWS endpoint. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nt ingestion One getSchedule/GetUserAvailability call per user batch; merged contiguous busy blocks set the same 'calendar' presence claim the legacy service uses (manual DND protection and status restore come from the presence engine). No event subjects or details are requested or stored in this mode. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… sync and diagnostics calendar-sync.test-connection (optional probe mailbox proves impersonation/ calendar access), calendar-sync.run, calendar-sync.status and calendar-sync.user-status — typed router, ajv schemas, manage-calendar-sync permission and outlook-calendar license gate. Delta tokens are never exposed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…EWS impersonation) and changeset Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…coping and admin test-connection action - GCC High / DoD (Azure Government) cloud endpoints, selectable per workspace - OAuth client credentials via certificate (RS256 client assertion with x5t thumbprint, built with node:crypto — no new dependencies); private key is a multiline secret setting - CalendarSync_User_Roles restricts sync to users holding the given roles - Test-connection action button in the admin settings group backed by the calendarSyncTestConnection meteor method (manage-calendar-sync permission) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SyncState is established before the initial snapshot (no missed changes), incremental runs page through Create/Update/Delete changes with client-side window filtering (out-of-window updates are treated as deletions), and ErrorInvalidSyncStateData falls back to a full snapshot — mirroring the Graph delta 410 recovery. Mailboxes too large to fast-forward degrade gracefully to windowed polling. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tion lifecycle
Per-user subscriptions on /users/{mailbox}/events are created and renewed
piggybacked on regular sync runs (no extra scheduler). The public receiver
endpoint answers the validation handshake as text/plain and authenticates
every notification against the per-subscription clientState secret before
triggering an immediate single-user delta sync. Notifications carry no
calendar data and webhooks remain a pure optimization over polling.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Looks like this PR is not ready to merge, because of the following issues:
Please fix the issues and try again If you have any trouble, please check the PR guidelines |
🦋 Changeset detectedLatest commit: 5117a4a The changes in this PR will be included in the next version bump. This PR includes changesets to release 5 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Milton Rucks seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account. You have signed the CLA already but the status is still pending? Let us recheck it. |
| ? new https.Agent({ | ||
| keepAlive: true, | ||
| maxSockets: 1, | ||
| ...(config.allowSelfSignedCerts && { rejectUnauthorized: false }), |
| } | ||
|
|
||
| function hmacMd5(key: Buffer, data: Buffer): Buffer { | ||
| return crypto.createHmac('md5', key).update(data).digest(); |
| throw new CalendarSyncError('invalid-certificate', 'The configured value is not a PEM-encoded certificate'); | ||
| } | ||
| const der = Buffer.from(match[1].replace(/\s+/g, ''), 'base64'); | ||
| return base64url(crypto.createHash('sha1').update(der).digest()); |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## develop #41326 +/- ##
===========================================
+ Coverage 69.04% 69.13% +0.09%
===========================================
Files 3757 3772 +15
Lines 147735 148673 +938
Branches 26395 26616 +221
===========================================
+ Hits 102000 102782 +782
- Misses 41234 41332 +98
- Partials 4501 4559 +58
Flags with carried forward coverage won't be shown. Click here to find out more. 🚀 New features to boost your workflow:
|
…ge lab setup Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Proposed changes (including videos or screenshots)
Adds a server-to-server calendar integration with Microsoft Outlook/Exchange (Premium,
outlook-calendarmodule). The server periodically fetches users' calendars using admin-provided credentials and imports them through the existing calendar service — users no longer need to authenticate against Outlook from their own clients. The legacy client/desktop-based integration is untouched and keeps working alongside it.Two providers behind one abstraction (
ICalendarSyncProvider):node:crypto) —calendarViewdelta queries with 410 recovery, Retry-After throttling backoff,getSchedulefree/busy, and national clouds (GCC High / DoD). No MSAL/Graph SDK.ApplicationImpersonation, NTLMv2 (vendored, incl. pure-TS MD4 — OpenSSL 3 removed MD4, so npm NTLM libs break on modern Node) or Basic auth over a pinned keep-alive socket, hand-rolled SOAP (FindItem/GetItem/GetUserAvailability/SyncFolderItemsincremental sync) parsed with the repo's existing@xmldom/xmldom. Zero new npm dependencies.Air-gap guarantee: with the EWS provider selected, no Microsoft cloud endpoint is ever contacted — enforced by construction and verified three ways in
airGap.spec.ts(static source scan, factory-level check, runtime full-sync URL assertion).Sync engine: cron-scheduled (configurable interval), batched with bounded parallelism, per-user sync state (
calendar_sync_statecollection) with delta-token lifecycle, idempotent upserts throughCalendarService.importkeyed on external id with aproviderdiscriminator +iCalUId, deletion/cancellation handling via snapshot diffing and delta removals, mailbox mapping via verified email or a user custom field, and optional role scoping.Modes:
full-events(default): subjects/times/bodies, matching legacy behavior.free-busy-only: drives presence from availability blocks without ingesting any event details (needs onlyCalendars.ReadBasicon Graph — verified least-privileged permission forgetSchedule).Presence reuses the exact legacy mechanism (
Presence.setActiveStateclaim withstatusId: 'calendar'), so manual-DND protection and prior-status restore come from the presence engine — no parallel pathway.Admin surface: new
Calendar_Syncsettings group (secrets astype: 'password', secret: true), a Test connection action button, and REST endpoints gated by a newmanage-calendar-syncpermission:calendar-sync.test-connection(optional probe mailbox proves impersonation/consent with actionable error codes),calendar-sync.run,calendar-sync.status,calendar-sync.user-status.Optional Graph webhooks (off by default): per-user change-notification subscriptions created/renewed piggybacked on sync runs; the public receiver validates the handshake and authenticates every notification against a per-subscription
clientStatesecret before triggering a single-user delta sync. Polling always continues — webhooks are an optimization, never a dependency.Admin guide:
docs/features/server-calendar-sync.md(Entra ID registration, admin consent,ApplicationAccessPolicyscoping, EWS service account +New-ManagementRoleAssignment, TLS/custom CA guidance, troubleshooting codes).Issue(s)
N/A — internal feature work.
Steps to test or reproduce
Calendars.Readapplication permission + admin consent; fill in tenant/client id and a secret (or certificate + private key); click Test connection.ApplicationImpersonation, set the EWS endpoint URL/credentials (NTLM default); click Test connection.calendar-events.listand in the calendar UI, and busy meetings set users to busy (manual DND is never overridden).GET /api/v1/calendar-sync.statusandcalendar-sync.user-status?userId=…for diagnostics;POST /api/v1/calendar-sync.runtriggers an immediate run.cd apps/meteor && yarn mocha --config ./.mocharc.js 'tests/unit/server/ee/calendarSync/*.spec.ts'(~130 tests, incl. RFC 1320 / [MS-NLMP] crypto vectors and the air-gap suite).Further comments
serverFetch: NTLM authenticates the TCP connection, so the handshake must own a pinned keep-alive agent. TLS override is a scoped, explicit admin opt-in (never global); docs recommendNODE_EXTRA_CA_CERTSfor private CAs.CalendarService.import(community code) was extended additively to pass throughprovider/iCalUId; legacy behavior is unchanged.outlook-calendarlicense module (a new module would need SKU provisioning); confirm with product if a distinct module is preferred.tests/end-to-end/api/(need a licensed instance) and a dedicated admin UI panel beyond the settings group.🤖 Generated with Claude Code